extension MySQLPacket {
    /// Protocol::HandshakeResponse
    ///
    /// Depending on the servers support for the CLIENT_PROTOCOL_41 capability and the clients
    /// understanding of that flag the client has to send either a Protocol::HandshakeResponse41
    /// or Protocol::HandshakeResponse320.
    ///
    /// Handshake Response Packet sent by 4.1+ clients supporting CLIENT_PROTOCOL_41 capability,
    /// if the server announced it in its Initial Handshake Packet. Otherwise (talking to an old server)
    /// the Protocol::HandshakeResponse320 packet must be used.
    ///
    /// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse
    public struct HandshakeResponse41: MySQLPacketDecodable, MySQLPacketEncodable {
        /// capability_flags (4)
        /// capability flags of the client as defined in Protocol::CapabilityFlags
        public var capabilities: MySQLProtocol.CapabilityFlags
        
        /// max_packet_size (4)
        /// max size of a command packet that the client wants to send to the server
        public var maxPacketSize: UInt32
        
        /// character_set (1)
        /// connection's default character set as defined in Protocol::CharacterSet.
        public var characterSet: MySQLProtocol.CharacterSet
        
        /// username (string.fix_len)
        /// name of the SQL account which client wants to log in this string should be interpreted using the character set indicated by character set field.
        public var username: String
        
        /// auth-response (string.NUL)
        /// opaque authentication response data generated by Authentication Method indicated by the plugin name field.
        public var authResponse: ByteBuffer
        
        /// database (string.NUL)
        /// initial database for the connection -- this string should be interpreted using the character set indicated by character set field.
        public var database: String
        
        /// auth plugin name (string.NUL)
        /// the Authentication Method used by the client to generate auth-response value in this packet. This is an UTF-8 string.
        public var authPluginName: String
        
        /// Creates a new `MySQLHandshakeResponse41`
        public init(
            capabilities: MySQLProtocol.CapabilityFlags,
            maxPacketSize: UInt32,
            characterSet: MySQLProtocol.CharacterSet,
            username: String,
            authResponse: ByteBuffer,
            database: String,
            authPluginName: String
        ) {
            self.capabilities = capabilities
            self.maxPacketSize = maxPacketSize
            self.characterSet = characterSet
            self.username = username
            self.authResponse = authResponse
            self.database = database
            self.authPluginName = authPluginName
        }
        
        /// `MySQLPacketEncodable` conformance.
        public func encode(to packet: inout MySQLPacket, capabilities _ : MySQLProtocol.CapabilityFlags) throws {
            packet.payload.writeInteger(self.capabilities.general, endianness: .little)
            packet.payload.writeInteger(maxPacketSize, endianness: .little)
            packet.payload.writeInteger(self.characterSet.rawValue, endianness: .little)
            /// string[23]     reserved (all [0])
            packet.payload.writeBytes([UInt8](repeating: 0, count: 23))
            packet.payload.writeNullTerminatedString(self.username)
            if self.capabilities.contains(.CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) {
                var responseCopy = authResponse
                packet.payload.writeLengthEncodedSlice(&responseCopy)
            } else if self.capabilities.contains(.CLIENT_SECURE_CONNECTION) {
                assert(self.authResponse.readableBytes <= UInt8.max, "auth response too large")
                packet.payload.writeInteger(UInt8(self.authResponse.readableBytes), endianness: .little)
                var authResponseCopy = self.authResponse
                packet.payload.writeBuffer(&authResponseCopy)
            } else {
                var authResponseCopy = self.authResponse
                packet.payload.writeBuffer(&authResponseCopy)
                // null terminated
                packet.payload.writeInteger(0, as: UInt8.self)
            }
            if self.capabilities.contains(.CLIENT_CONNECT_WITH_DB) {
                packet.payload.writeNullTerminatedString(self.database)
            } else {
                assert(self.database == "", "CLIENT_CONNECT_WITH_DB not enabled")
            }
            if self.capabilities.contains(.CLIENT_PLUGIN_AUTH) {
                packet.payload.writeNullTerminatedString(self.authPluginName)
            } else {
                assert(self.authPluginName == "", "CLIENT_PLUGIN_AUTH not enabled")
            }
            assert(self.capabilities.contains(.CLIENT_CONNECT_ATTRS) == false, "CLIENT_CONNECT_ATTRS not supported")
        }

        /// `MySQLPacketDecodable` conformance.
        public static func decode(from packet: inout MySQLPacket, capabilities _: MySQLProtocol.CapabilityFlags) throws -> HandshakeResponse41 {
            guard let rawClientCapabilities = packet.payload.readInteger(endianness: .little, as: UInt32.self) else {
                throw Error.missingCapabilities
            }
            let clientCapabilities = MySQLProtocol.CapabilityFlags(rawValue: UInt64(rawClientCapabilities))
            guard let maxPacketSize = packet.payload.readInteger(endianness: .little, as: UInt32.self) else {
                throw Error.missingPacketSize
            }
            guard let rawCharacterSet = packet.payload.readInteger(endianness: .little, as: UInt8.self) else {
                throw Error.missingCharacterSet
            }
            let characterSet = MySQLProtocol.CharacterSet(rawValue: rawCharacterSet)
            guard let reservedData = packet.payload.readBytes(length: 23) else {
                throw Error.missingReservedData
            }
            guard !reservedData.contains(where: { $0 != 0 }) else {
                throw Error.invalidReservedData(reservedData)
            }
            guard let username = packet.payload.readNullTerminatedString() else {
                throw Error.missingUsername
            }
            let authResponse: ByteBuffer
            if clientCapabilities.contains(.CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) {
                guard let lenencAuthResponse = packet.payload.readLengthEncodedSlice() else {
                    throw Error.missingAuthResponse
                }
                authResponse = lenencAuthResponse
            } else if clientCapabilities.contains(.CLIENT_SECURE_CONNECTION) {
                guard let authResponseLength = packet.payload.readInteger(endianness: .little, as: UInt8.self) else {
                    throw Error.missingAuthResponse
                }
                guard let secAuthResponse = packet.payload.readBytes(length: numericCast(authResponseLength)) else {
                    throw Error.missingAuthResponse
                }
                authResponse = .init(bytes: secAuthResponse)
            } else {
                guard let insecAuthResponse = packet.payload.readNullTerminatedBytes() else {
                    throw Error.missingAuthResponse
                }
                authResponse = .init(bytes: insecAuthResponse)
            }
            let database: String?
            if clientCapabilities.contains(.CLIENT_CONNECT_WITH_DB) {
                guard let databaseName = packet.payload.readNullTerminatedString() else {
                    throw Error.missingDatabaseName
                }
                database = databaseName
            } else {
                database = nil
            }
            let authPlugin: String?
            if clientCapabilities.contains(.CLIENT_PLUGIN_AUTH) {
                guard let authPluginName = packet.payload.readNullTerminatedString() else {
                    throw Error.missingAuthPluginName
                }
                authPlugin = authPluginName
            } else {
                authPlugin = nil
            }
            
            return .init(
                capabilities: clientCapabilities,
                maxPacketSize: maxPacketSize,
                characterSet: characterSet,
                username: username,
                authResponse: authResponse,
                database: database ?? "",
                authPluginName: authPlugin ?? ""
            )
        }

        public enum Error: Swift.Error {
            case missingCapabilities
            case missingPacketSize
            case missingCharacterSet
            case missingReservedData
            case invalidReservedData([UInt8])
            case missingUsername
            case missingAuthResponse
            case missingDatabaseName
            case missingAuthPluginName
        }
    }
}

extension ByteBuffer {
    public mutating func readNullTerminatedBytes() -> [UInt8]? {
        var copy = self
        while let byte = copy.readInteger(as: UInt8.self), byte != 0x00 {
            continue
        }
        defer { self.moveReaderIndex(forwardBy: 1) }
        return self.readBytes(length: (self.readableBytes - copy.readableBytes) - 1)
    }
}

